Passed
Pull Request — master (#2)
by Muhammad Dyas
01:24
created

index.ts ➔ showConfigurationForm   F

Complexity

Conditions 34

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
dl 0
loc 19
rs 0
c 0
b 0
f 0
cc 34

How to fix   Complexity   

Complexity

Complex classes like index.ts ➔ showConfigurationForm often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import {HttpFunction} from '@google-cloud/functions-framework/build/src/functions';
2
3
import {
4
  buildConfigurationForm,
5
} from './config-form';
6
import {buildVoteCard} from './vote-card';
7
import {saveVotes} from './helpers/vote';
8
import {buildAddOptionForm} from './add-option-form';
9
import {callMessageApi} from './helpers/api';
10
import {addOptionToState} from './helpers/option';
11
import {buildActionResponseStatus} from './helpers/response';
12
import {MAX_NUM_OF_OPTIONS} from './config/default';
13
import {chat_v1 as chatV1} from 'googleapis/build/src/apis/chat/v1';
14
import {Voter, Votes} from './helpers/interfaces';
15
import {PollCard} from './cards/PollCard';
16
import {CommandHandler} from './handlers/CommandHandler';
17
import {MessageHandler} from './handlers/MessageHandler';
18
19
export const app: HttpFunction = async (req, res) => {
20
  if (!(req.method === 'POST' && req.body)) {
21
    res.status(400).send('');
22
  }
23
  const buttonCard: chatV1.Schema$CardWithId = {
24
    'cardId': 'welcome-card',
25
    'card': {
26
      'sections': [
27
        {
28
          'widgets': [
29
            {
30
              'buttonList': {
31
                'buttons': [
32
                  {
33
                    'text': 'Create Poll',
34
                    'onClick': {
35
                      'action': {
36
                        'function': 'show_form',
37
                        'interaction': 'OPEN_DIALOG',
38
                        'parameters': [],
39
                      },
40
                    },
41
                  },
42
                  {
43
                    'text': 'Terms and Conditions',
44
                    'onClick': {
45
                      'openLink': {
46
                        'url': 'https://absolute-poll.yaskur.com/terms-and-condition',
47
                      },
48
                    },
49
                  },
50
                  {
51
                    'text': 'Contact Us',
52
                    'onClick': {
53
                      'openLink': {
54
                        'url': 'https://absolute-poll.yaskur.com/contact-us',
55
                      },
56
                    },
57
                  },
58
                ],
59
              },
60
            },
61
          ],
62
        },
63
      ],
64
    },
65
  };
66
  const event = req.body;
67
  console.log(event.type,
68
    event.common?.invokedFunction || event.message?.slashCommand?.commandId || event.message?.argumentText,
69
    event.user.displayName, event.user.email, event.space.type, event.space.name);
70
  // console.log(JSON.stringify(event.message.cardsV2));
71
  console.log(JSON.stringify(event.message));
72
  console.log(JSON.stringify(event.user));
73
  let reply: chatV1.Schema$Message = {
74
    thread: event.message.thread,
75
    actionResponse: {
76
      type: 'NEW_MESSAGE',
77
    },
78
    text: 'Hi! To create a poll, you can use the */poll* command. \n \n' +
79
      'Alternatively, you can create poll by mentioning me with question and answers. ' +
80
      'e.g *@Absolute Poll "Your Question" "Answer 1" "Answer 2"*',
81
  };
82
  // Dispatch slash and action events
83
  if (event.type === 'MESSAGE') {
84
    const message = event.message;
85
    if (message.slashCommand?.commandId) {
86
      reply = new CommandHandler(event).process();
87
    } else if (message.text) {
88
      reply = new MessageHandler(event).process();
89
    }
90
  } else if (event.type === 'CARD_CLICKED') {
91
    const action = event.common?.invokedFunction;
92
    if (action === 'start_poll') {
93
      reply = await startPoll(event);
94
    } else if (action === 'vote') {
95
      reply = recordVote(event);
96
    } else if (action === 'add_option_form') {
97
      reply = addOptionForm(event);
98
    } else if (action === 'add_option') {
99
      reply = await saveOption(event);
100
    } else if (action === 'show_form') {
101
      // todo: show form using new card generator
102
    }
103
  } else if (event.type === 'ADDED_TO_SPACE') {
104
    const message: chatV1.Schema$Message = {
105
      text: undefined,
106
      cardsV2: undefined,
107
    };
108
    const spaceType = event.space.type;
109
    if (spaceType === 'ROOM') {
110
      message.text = 'Hi there! I\'d be happy to assist you in creating polls to improve collaboration and ' +
111
        'decision-making efficiency on Google Chat™.\n' +
112
        '\n' +
113
        'To create a poll, simply use the */poll* command or click on the "Create Poll" button below. ' +
114
        'You can also test our app in a direct message if you prefer.\n' +
115
        '\n' +
116
        'Alternatively, you can ' +
117
        'You can also test our app in a direct message if you prefer.\n' +
118
        '\n' +
119
        'We hope you find our service useful and please don\'t hesitate to contact us ' +
120
        'if you have any questions or concerns.';
121
    } else if (spaceType === 'DM') {
122
      message.text = 'Hey there! ' +
123
        'Before creating a poll in a group space, you can test it out here in a direct message.\n' +
124
        '\n' +
125
        'To create a poll, you can use the */poll* command or click on the "Create Poll" button below.\n' +
126
        '\n' +
127
        'Thank you for using our bot. We hope that it will prove to be a valuable tool for you and your team.\n' +
128
        '\n' +
129
        'Don\'t hesitate to reach out if you have any questions or concerns in the future.' +
130
        ' We are always here to help you and your team';
131
    }
132
133
    message.cardsV2 = [buttonCard];
134
135
    reply = {
136
      actionResponse: {
137
        type: 'NEW_MESSAGE',
138
      },
139
      ...message,
140
    };
141
  }
142
  res.json(reply);
143
};
144
145
146
/**
147
 * Handle the custom start_poll action.
148
 *
149
 * @param {object} event - chat event
150
 * @returns {object} Response to send back to Chat
151
 */
152
async function startPoll(event: chatV1.Schema$DeprecatedEvent) {
153
  // Get the form values
154
  const formValues = event.common?.formInputs;
155
  const topic = formValues?.['topic']?.stringInputs?.value?.[0]?.trim() ?? '';
156
  const isAnonymous = formValues?.['is_anonymous']?.stringInputs?.value?.[0] === '1';
157
  const allowAddOption = formValues?.['allow_add_option']?.stringInputs?.value?.[0] === '1';
158
  const choices = [];
159
  const votes: Votes = {};
160
161
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
162
    const choice = formValues?.[`option${i}`]?.stringInputs?.value?.[0]?.trim();
163
    if (choice) {
164
      choices.push(choice);
165
      votes[i] = [];
166
    }
167
  }
168
169
  if (!topic || choices.length === 0) {
170
    // Incomplete form submitted, rerender
171
    const dialog = buildConfigurationForm({
172
      topic,
173
      choices,
174
    });
175
    return {
176
      actionResponse: {
177
        type: 'DIALOG',
178
        dialogAction: {
179
          dialog: {
180
            body: dialog,
181
          },
182
        },
183
      },
184
    };
185
  }
186
  const pollCard = new PollCard({
187
    topic: topic, choiceCreator: undefined,
188
    author: event.user,
189
    choices: choices,
190
    votes: votes,
191
    anon: isAnonymous,
192
    optionable: allowAddOption,
193
  }).createCardWithId();
194
  // Valid configuration, make the voting card to display in the space
195
  const message = {
196
    cardsV2: [pollCard],
197
  };
198
  const request = {
199
    parent: event.space?.name,
200
    requestBody: message,
201
  };
202
  const apiResponse = await callMessageApi('create', request);
203
  if (apiResponse) {
204
    return buildActionResponseStatus('Poll started.', 'OK');
205
  } else {
206
    return buildActionResponseStatus('Failed to start poll.', 'UNKNOWN');
207
  }
208
}
209
210
/**
211
 * Handle the custom vote action. Updates the state to record
212
 * the user's vote then rerenders the card.
213
 *
214
 * @param {object} event - chat event
215
 * @returns {object} Response to send back to Chat
216
 */
217
function recordVote(event: chatV1.Schema$DeprecatedEvent) {
218
  const parameters = event.common?.parameters;
219
  if (!(parameters?.['index'])) {
220
    throw new Error('Index Out of Bounds');
221
  }
222
  const choice = parseInt(parameters['index']);
223
  const userId = event.user?.name ?? '';
224
  const userName = event.user?.displayName ?? '';
225
  const voter: Voter = {uid: userId, name: userName};
226
  const state = JSON.parse(parameters['state']);
227
228
  // Add or update the user's selected option
229
  state.votes = saveVotes(choice, voter, state.votes, state.anon);
230
231
  const card = new PollCard(state);
232
  return {
233
    thread: event.message?.thread,
234
    actionResponse: {
235
      type: 'UPDATE_MESSAGE',
236
    },
237
    cardsV2: [card.createCardWithId()],
238
  };
239
}
240
241
/**
242
 * Opens and starts a dialog that allows users to add details about a contact.
243
 *
244
 * @param {object} event the event object from Google Chat.
245
 *
246
 * @returns {object} open a dialog.
247
 */
248
function addOptionForm(event: chatV1.Schema$DeprecatedEvent) {
249
  const card = event.message!.cardsV2?.[0]?.card;
250
  // @ts-ignore: because too long
251
  const stateJson = (card.sections[0].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value || card.sections[1].widgets[0].decoratedText?.button?.onClick?.action?.parameters[0].value) ?? '';
252
  const state = JSON.parse(stateJson);
253
  const dialog = buildAddOptionForm(state);
254
  return {
255
    actionResponse: {
256
      type: 'DIALOG',
257
      dialogAction: {
258
        dialog: {
259
          body: dialog,
260
        },
261
      },
262
    },
263
  };
264
}
265
;
266
267
/**
268
 * Handle the custom vote action. Updates the state to record
269
 * the user's vote then rerenders the card.
270
 *
271
 * @param {chatV1.Schema$DeprecatedEvent} event - chat event
272
 * @returns {object} Response to send back to Chat
273
 */
274
async function saveOption(event: chatV1.Schema$DeprecatedEvent) {
275
  const userName = event.user?.displayName ?? '';
276
  const state = getEventPollState(event);
277
  const formValues = event.common?.formInputs;
278
  const optionValue = formValues?.['value']?.stringInputs?.value?.[0]?.trim() || '';
279
  addOptionToState(optionValue, state, userName);
280
281
  const card = buildVoteCard(state);
282
  const message = {
283
    cardsV2: [card],
284
  };
285
  const request = {
286
    name: event.message!.name,
287
    requestBody: message,
288
    updateMask: 'cardsV2',
289
  };
290
  const apiResponse = await callMessageApi('update', request);
291
  if (apiResponse) {
292
    return buildActionResponseStatus('Option is added', 'OK');
293
  } else {
294
    return buildActionResponseStatus('Failed to add option.', 'UNKNOWN');
295
  }
296
}
297
298
function getEventPollState(event: chatV1.Schema$DeprecatedEvent) {
299
  const parameters = event.common?.parameters;
300
  const state = parameters?.['state'];
301
  if (!state) {
302
    throw new ReferenceError('no valid state in the event');
303
  }
304
  return JSON.parse(state);
305
}
306